Skip to content

feat: add support for deny by default access controls#852

Open
steveiliop56 wants to merge 9 commits into
mainfrom
feat/deny-by-default-acls
Open

feat: add support for deny by default access controls#852
steveiliop56 wants to merge 9 commits into
mainfrom
feat/deny-by-default-acls

Conversation

@steveiliop56
Copy link
Copy Markdown
Member

@steveiliop56 steveiliop56 commented May 12, 2026

Solves #850


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Summary by CodeRabbit

Release Notes

  • New Features

    • Added centralized policy engine for access control authorization decisions.
    • Policy engine consolidates user allow/block, group membership, path-based authentication, and IP filtering checks.
  • Configuration

    • Added acls.policy configuration option (defaults to "allow") to customize access control behavior.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a PolicyEngine abstraction that extracts ACL authorization logic from AuthService into composable rule implementations. New ACLS configuration, refactored AccessControlsService using model.Config, updated ProxyController to evaluate authorization through PolicyEngine rules, and comprehensive test coverage for all components.

Changes

Policy engine & ACL rule extraction

Layer / File(s) Summary
Configuration models and test fixtures
internal/model/config.go, internal/test/test.go
Add ACLS config type with policy field, document label-provider none option, and populate test configs with ACL policy and three sample app entries for path/user/IP testing.
PolicyEngine: types, initialization, and evaluation
internal/service/policy_engine.go, internal/service/policy_engine_test.go
Define Policy/Effect enums, Rule interface contract, ACLContext input shape with IP/path/user/ACLs, PolicyEngine with rule registry and effect-to-access conversion, and unit tests for initialization and policy semantics.
ACL rule implementations
internal/service/access_controls_rules.go
Implement six rule types (UserAllowed, OAuthGroup, LDAPGroup, AuthEnabled, IPAllowed, IPBypassed) with complete logic for whitelist/block/bypass matching, group membership checking, path regex evaluation, and CIDR blocking with logging on invalid configuration.
ACL rule unit tests
internal/service/access_controls_rules_test.go
Table-driven tests for each rule covering OAuth/email whitelist, group matching, path enable/disable with block/allow precedence, IP block/allow/bypass, and error handling for invalid filters and regex compilation.
AccessControlsService: config-driven ACL lookups
internal/service/access_controls_service.go, internal/service/access_controls_service_test.go
Refactor service to use model.Config instead of static map, perform two-pass domain/subdomain lookup in config.Apps, fall back to label provider, and add tests with mock label provider covering static lookup and provider fallback.
Bootstrap: services initialization and wiring
internal/bootstrap/app_bootstrap.go, internal/bootstrap/service_bootstrap.go, internal/bootstrap/router_bootstrap.go
Add policyEngine field to Services struct, extract label-provider selection into getLabelProvider helper supporting none/auto/explicit modes, extract policy-engine initialization and rule registration into setupPolicyEngine helper, update router to pass policyEngine to NewProxyController.
ProxyController: policy-engine-driven authorization
internal/controller/proxy_controller.go, internal/controller/proxy_controller_test.go
Add policyEngine field and constructor parameter, build ACLContext with client IP/path, replace direct AuthService/AccessControlsService authorization calls with PolicyEngine rule evaluations for IP bypass, auth enabled, IP allowed, user allowed, and group membership checks; update tests.
Utils helpers and AuthService cleanup
internal/utils/security_utils.go, internal/utils/security_utils_test.go, internal/service/auth_service.go, internal/service/docker_service.go
Replace FilterIP with CheckIPFilter supporting CIDR validation, refactor CheckFilter to return error for empty filters and regex compilation failure, remove six authorization helper methods from AuthService, update IsEmailWhitelisted error handling, refactor DockerService two-pass matching, remove gin and regexp imports.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • tinyauthapp/tinyauth#731: Refactors proxy controller tests, which overlap with main PR's test updates for wiring PolicyEngine.
  • tinyauthapp/tinyauth#844: Modifies ProxyController wiring and bootstrap router initialization overlapping with main PR's policyEngine dependency injection.
  • tinyauthapp/tinyauth#422: Modifies ACL data flow in ProxyController and AccessControlsService, directly connected to main PR's refactored authorization routing through PolicyEngine.

Suggested labels

size:XXL, architecture, refactoring, policy-engine

Suggested reviewers

  • Rycochet

Poem

🐰 A policy engine hops into view,
Rules in a registry, clean and new,
IP and OAuth, LDAP crews,
AuthService freed from its constraint blues,
Abstain, Allow, Deny—the trio that flew! 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main objective of the pull request: implementing support for deny-by-default access control policies.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/deny-by-default-acls

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label May 12, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
internal/controller/proxy_controller_test.go (1)

25-25: ⚡ Quick win

Add tests for the new deny/block policy.

The shared cfg from test.CreateTestConfigs(t) doesn't set Auth.ACLS.Policy, so the entire deny-by-default code path introduced in this PR is never executed by TestProxyController. The existing happy-path/path-allow/ip-bypass/user-allow cases all bypass policyResult either via explicit filters or via the bypass/auth-disabled short circuits.

Consider adding at least:

  • A test where cfg.Auth.ACLS.Policy = "block" and a request hits a domain with no matching Apps entry — assert it's denied at the IP allow step.
  • A test where cfg.Auth.ACLS.Policy = "block" and a request hits a domain with an explicit IP.Allow (or Users.Allow) — assert it succeeds.
  • A test with an invalid policy string (e.g., "deny", "") — assert it logs a warning and behaves as the documented default ("allow"). This will also catch the fallback regression flagged in NewAccessControlsService.

Also applies to: 367-367

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/proxy_controller_test.go` at line 25, The tests for
TestProxyController never exercise the new deny-by-default policy because
test.CreateTestConfigs(t) doesn't set cfg.Auth.ACLS.Policy; add new unit tests
that set cfg.Auth.ACLS.Policy = "block" and exercise the code paths around
policyResult: (1) a request to a domain with no matching Apps entry should be
denied at the IP allow step, (2) a request to a domain with an explicit IP.Allow
or Users.Allow should succeed, and (3) a test with an invalid policy string
(e.g., "deny" or "") should assert a warning is logged and behavior falls back
to documented default ("allow") to catch the NewAccessControlsService fallback
regression. Ensure tests reference and assert on the same controller flow
exercised by TestProxyController so policyResult is evaluated rather than
bypassed.
internal/controller/proxy_controller.go (1)

53-72: ⚡ Quick win

Remove unused auth field and constructor parameter from ProxyController.

After the refactor, all authorization checks in proxyHandler delegate to controller.acls (IP bypass, auth-enabled status, user allowed, groups). The auth field on the struct and corresponding auth parameter in NewProxyController are never referenced. Remove them to keep the dependency surface minimal and make intent clearer. Update the wiring in internal/bootstrap/router_bootstrap.go:47 and test setup accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/proxy_controller.go` around lines 53 - 72, The
ProxyController struct and constructor still include an unused auth dependency:
remove the auth field from the ProxyController struct and remove the auth
parameter (type *service.AuthService) from NewProxyController signature and its
struct literal, then update all call sites that invoke NewProxyController to
stop passing the auth argument (including bootstrap/router wiring and tests) so
the code compiles with the reduced dependency surface; ensure any imports or
test fixtures related only to auth are cleaned up.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/model/config.go`:
- Around line 230-232: The ACLSConfig description string is inconsistent and has
a typo: change "deny-by-defaut" to "deny-by-default" and make the allowed values
consistent with accessControlPolicyFromString by either (preferred) switching
the internal parser/constants in access_controls_service.go (and any related
constants) to accept the YAML value "deny" instead of "block", or conversely
update this description to list "allow and block"; specifically modify
ACLSConfig (type ACLSConfig) description text and update
accessControlPolicyFromString (and its associated policy constants) so the
accepted policy string matches the user-facing value ("deny") throughout the
codebase.

In `@internal/service/access_controls_service.go`:
- Around line 213-215: The current append usage can mutate the shared backing
arrays for service.config.Auth.IP.Block/Allow; to fix, create new slices before
concatenation so you never write into the shared slice: allocate a fresh slice
(e.g., with make or by starting from a nil-typed slice) and copy or append the
global entries then the per-app entries to produce blockedIps and allowedIPs;
update the code that constructs blockedIps and allowedIPs (the lines using
append with service.config.Auth.IP.Block and acls.IP.Block, and similarly for
.Allow) to use the safe allocation/copy approach (or slices.Concat if you target
Go ≥1.22).
- Around line 41-68: NewAccessControlsService sets service.policy incorrectly
when accessControlPolicyFromString returns ok==false: it first assigns
service.policy = PolicyAllow then later logs and overwrites service.policy with
the empty/local variable policy, leaving an invalid value. Fix by using the
validated policy variable (or set policy = PolicyAllow when !ok) before any
logging and before assigning into service.policy; specifically adjust
NewAccessControlsService to set policy = PolicyAllow when
accessControlPolicyFromString returns ok==false (and keep the warning log), then
use that policy for the subsequent if/else logging and final service.policy
assignment so service.policy and the logged policy remain consistent
(references: NewAccessControlsService, accessControlPolicyFromString,
service.policy, policy, PolicyAllow).
- Around line 106-125: IsUserAllowed currently treats an existing ACL with empty
users filters as allowing access because utils.CheckFilter("", username) returns
true; change IsUserAllowed (and similarly handle the OAuth whitelist if desired)
to detect when both acls.Users.Block and acls.Users.Allow are empty and route
that case through service.policyResult(true) instead of calling
utils.CheckFilter, so the global policy (policyResult) is respected when an ACL
exists but contains no user filters; use the symbols IsUserAllowed,
acls.Users.Block, acls.Users.Allow, utils.CheckFilter, and service.policyResult
to locate and modify the logic.

---

Nitpick comments:
In `@internal/controller/proxy_controller_test.go`:
- Line 25: The tests for TestProxyController never exercise the new
deny-by-default policy because test.CreateTestConfigs(t) doesn't set
cfg.Auth.ACLS.Policy; add new unit tests that set cfg.Auth.ACLS.Policy = "block"
and exercise the code paths around policyResult: (1) a request to a domain with
no matching Apps entry should be denied at the IP allow step, (2) a request to a
domain with an explicit IP.Allow or Users.Allow should succeed, and (3) a test
with an invalid policy string (e.g., "deny" or "") should assert a warning is
logged and behavior falls back to documented default ("allow") to catch the
NewAccessControlsService fallback regression. Ensure tests reference and assert
on the same controller flow exercised by TestProxyController so policyResult is
evaluated rather than bypassed.

In `@internal/controller/proxy_controller.go`:
- Around line 53-72: The ProxyController struct and constructor still include an
unused auth dependency: remove the auth field from the ProxyController struct
and remove the auth parameter (type *service.AuthService) from
NewProxyController signature and its struct literal, then update all call sites
that invoke NewProxyController to stop passing the auth argument (including
bootstrap/router wiring and tests) so the code compiles with the reduced
dependency surface; ensure any imports or test fixtures related only to auth are
cleaned up.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d27ab4c6-72fd-4514-9274-045ba1cf85e8

📥 Commits

Reviewing files that changed from the base of the PR and between a9eac7e and 3fd5627.

📒 Files selected for processing (7)
  • internal/bootstrap/service_bootstrap.go
  • internal/controller/proxy_controller.go
  • internal/controller/proxy_controller_test.go
  • internal/model/config.go
  • internal/service/access_controls_service.go
  • internal/service/auth_service.go
  • internal/test/test.go
💤 Files with no reviewable changes (1)
  • internal/service/auth_service.go

Comment thread internal/model/config.go Outdated
Comment thread internal/service/access_controls_service.go
Comment thread internal/service/access_controls_service.go Outdated
Comment thread internal/service/access_controls_service.go Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/service/access_controls_service.go (1)

71-82: 💤 Low value

Prefer the explicit local-copy pattern when storing a pointer to the loop value.

appAcls = &config takes the address of the range loop variable. Although the immediate break makes this functionally safe, the project convention is to materialize an explicit local copy to make intent obvious and avoid loop-variable capture confusion.

♻️ Proposed refactor
 	var appAcls *model.App
 	for app, config := range service.config.Apps {
+		appConfig := config
 		if config.Config.Domain == domain {
 			service.log.App.Debug().Str("name", app).Msg("Found matching container by domain")
-			appAcls = &config
+			appAcls = &appConfig
 			break // If we find a match by domain, we can stop searching
 		}
 
 		if strings.SplitN(domain, ".", 2)[0] == app {
 			service.log.App.Debug().Str("name", app).Msg("Found matching container by app name")
-			appAcls = &config
+			appAcls = &appConfig
 			break // If we find a match by app name, we can stop searching
 		}
 	}

Based on learnings: "prefer the explicit local copy pattern (e.g., result := config; return &result) rather than returning &<range-variable> directly … the project prefers the explicit copy for clarity and to avoid reviewer confusion about loop-variable capture semantics."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/access_controls_service.go` around lines 71 - 82, The code
takes the address of the range loop variable (config) when assigning appAcls
(appAcls = &config); change this to the explicit local-copy pattern to avoid
loop-variable capture confusion: create a new local variable (e.g., cfg :=
config) and assign appAcls = &cfg instead for both matches (the domain check and
the app-name check) while keeping the break logic and using the existing
service.config.Apps, appAcls, and config identifiers to locate the spots to
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/service/access_controls_service.go`:
- Around line 173-205: The Path.Block branch in
AccessControlsService.IsAuthEnabled currently uses a negation (if
!regex.MatchString(uri) { return false }) which inverts the intended behavior;
remove the negation so that matching the block regex disables auth (i.e., if
regex.MatchString(uri) { return false }). Also avoid compiling regexes on every
call to IsAuthEnabled: precompile and cache the regex objects for
acls.Path.Block and acls.Path.Allow (either during service initialization or by
adding compiled fields on the model.App/ACL struct) and use those cached
*regexp.Regexp instances in IsAuthEnabled; preserve existing error logging via
service.log.App.Error() when compilation fails during initialization or when
setting the ACL.

---

Nitpick comments:
In `@internal/service/access_controls_service.go`:
- Around line 71-82: The code takes the address of the range loop variable
(config) when assigning appAcls (appAcls = &config); change this to the explicit
local-copy pattern to avoid loop-variable capture confusion: create a new local
variable (e.g., cfg := config) and assign appAcls = &cfg instead for both
matches (the domain check and the app-name check) while keeping the break logic
and using the existing service.config.Apps, appAcls, and config identifiers to
locate the spots to change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a7e25ccc-eb0b-4e59-b3b4-8d0fc39ae11f

📥 Commits

Reviewing files that changed from the base of the PR and between 3fd5627 and b9abab2.

📒 Files selected for processing (3)
  • internal/model/config.go
  • internal/service/access_controls_service.go
  • internal/test/test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/test/test.go
  • internal/model/config.go

Comment thread internal/service/access_controls_service.go Outdated
@arcoast
Copy link
Copy Markdown

arcoast commented May 14, 2026

Whilst I can't help with code reviews, I 'm more than happy to build locally and help test this if that's of any use to you @steveiliop56

@steveiliop56
Copy link
Copy Markdown
Member Author

@arcoast it would be better to wait for an alpha/beta release. But if you like, you can test locally using the development mode and let me know if everything works correctly. Any feedback is appreciated!

@arcoast
Copy link
Copy Markdown

arcoast commented May 15, 2026

I don't mind, whatever is most useful for you.

If you do want me to test then let me know what the labels/envvars required.

@steveiliop56
Copy link
Copy Markdown
Member Author

This change is quite simple, all you need to do is set TINYAUTH_AUTH_ACLS_POLICY=deny and you are done. Tinyauth will reject everything and everyone unless specifically authorized.

Rycochet
Rycochet previously approved these changes May 16, 2026
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label May 16, 2026
Comment thread internal/bootstrap/service_bootstrap.go Outdated
Comment thread internal/service/access_controls_service.go Outdated
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels May 17, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/controller/proxy_controller.go (1)

114-133: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't collapse rule evaluation failures into plain allow/deny results.

These checks now only consume a bool, so malformed ACL path/IP rules can no longer reach handleError(...) as configuration errors. That turns bad ACLs into ordinary auth decisions and makes misconfiguration much harder to detect in production.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/proxy_controller.go` around lines 114 - 133, The rule
evaluation calls (policyEngine.Evaluate) are treating malformed rules as simple
booleans, hiding config errors; update the three evaluations for
service.RuleIPBypassed, service.RuleAuthEnabled, and service.RuleIPAllowed to
capture and check an error return (e.g., allowed, err :=
controller.policyEngine.Evaluate(...)); if err != nil call
controller.handleError(c, err) and return; only proceed to controller.setHeaders
and c.JSON or the !allowed branch when err is nil and the boolean result is
definitive. Ensure you propagate errors from the policy engine API (or change
Evaluate to return (bool, error) if needed) and keep existing handlers
(controller.setHeaders, controller.log.App.Debug, c.JSON) unchanged when no
error occurs.
♻️ Duplicate comments (1)
internal/service/access_controls_rules.go (1)

189-192: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid mutating shared ACL/config slice backing arrays during merge.

The current append merge can write into existing backing arrays (ctx.ACLs.IP.*) if capacity allows, causing cross-request leakage/corruption of filter state.

Safe merge without aliasing
-	// Merge the global and app IP filter
-	blockedIps := append(ctx.ACLs.IP.Block, rule.Config.Auth.IP.Block...)
-	allowedIPs := append(ctx.ACLs.IP.Allow, rule.Config.Auth.IP.Allow...)
+	// Merge the global and app IP filter (copy into fresh slices)
+	blockedIps := make([]string, 0, len(ctx.ACLs.IP.Block)+len(rule.Config.Auth.IP.Block))
+	blockedIps = append(blockedIps, ctx.ACLs.IP.Block...)
+	blockedIps = append(blockedIps, rule.Config.Auth.IP.Block...)
+
+	allowedIPs := make([]string, 0, len(ctx.ACLs.IP.Allow)+len(rule.Config.Auth.IP.Allow))
+	allowedIPs = append(allowedIPs, ctx.ACLs.IP.Allow...)
+	allowedIPs = append(allowedIPs, rule.Config.Auth.IP.Allow...)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/access_controls_rules.go` around lines 189 - 192, The merge
of IP lists using append (creating blockedIps and allowedIPs from
ctx.ACLs.IP.Block/Allow and rule.Config.Auth.IP.Block/Allow) can mutate the
backing arrays of the shared ctx.ACLs slices; allocate a fresh slice and copy
both source slices into it instead of appending into an existing backing
array—e.g. create a new slice with capacity equal to
len(ctx.ACLs.IP.Block)+len(rule.Config.Auth.IP.Block) (or start from
append([]string{}, ctx.ACLs.IP.Block...) and then append the rule slice), and do
the same for allowedIPs; update the variables blockedIps and allowedIPs
accordingly so ctx.ACLs.IP.* is never mutated.
🧹 Nitpick comments (4)
internal/utils/security_utils.go (1)

84-93: ⚡ Quick win

Regex filter should not fall through to comma-separated check.

When a regex filter is detected (wrapped in /...) but doesn't match, the code falls through to the comma-separated comparison. This creates an edge case where if input is literally the regex string itself (e.g., input "/^admin$/" against filter "/^admin$/"), it would match unexpectedly.

Additionally, filters like / or // would compile an empty regex that matches everything.

♻️ Proposed fix
 	if strings.HasPrefix(filter, "/") && strings.HasSuffix(filter, "/") {
+		if len(filter) < 3 {
+			return false, fmt.Errorf("invalid regex filter: empty pattern")
+		}
 		re, err := regexp.Compile(filter[1 : len(filter)-1])
 		if err != nil {
 			return false, fmt.Errorf("invalid regex filter: %w", err)
 		}
 
-		if re.MatchString(input) {
-			return true, nil
-		}
+		return re.MatchString(input), nil
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/utils/security_utils.go` around lines 84 - 93, When a filter is
wrapped in slashes (the code branch that checks strings.HasPrefix(filter, "/")
&& strings.HasSuffix(filter, "/")), ensure you treat it exclusively as a regex:
validate that the inner pattern is non-empty (e.g., len(filter) > 2 and
filter[1:len(filter)-1] != "") to avoid compiling "/" or "//" into an empty
regex, compile the pattern and if compilation fails return the error, and if
compilation succeeds but re.MatchString(input) is false return (false, nil)
immediately instead of falling through to the comma-separated logic; update the
existing handling around variables filter, input and the regexp.Compile call to
enforce these checks.
internal/service/access_control_rules_test.go (1)

12-181: ⚡ Quick win

Add nil-context/user-context regression cases for all rules.

Given the rule evaluators are security-critical request-path logic, add table rows for ctx == nil and ctx.UserContext == nil (where applicable) to assert deterministic Effect* outcomes and prevent panic regressions.

Also applies to: 183-309, 311-409, 411-518, 520-636, 638-702

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/access_control_rules_test.go` around lines 12 - 181, Add
regression table rows to TestUserAllowedRule (and the other rule tests
mentioned) to cover nil inputs: create two cases — one with ctx == nil and one
with ctx != nil but ctx.UserContext == nil — and assert the rule's Evaluate
method (UserAllowedRule.Evaluate) returns a deterministic Effect (use
EffectAbstain) for both; repeat the same two-row additions for each
corresponding test function covering the other rule types so Evaluate handles
nil ACLContext and nil UserContext without panicking.
internal/service/access_controls_service.go (1)

37-43: ⚡ Quick win

Use an explicit local copy when returning a pointer from for range.

Returning &config from the range variable works but is easy to misread and has loop-variable semantics pitfalls. Prefer a named local copy before taking the address.

Suggested pattern
-			appAcls = &config
+			result := config
+			appAcls = &result
@@
-			appAcls = &config
+			result := config
+			appAcls = &result

Based on learnings: prefer the explicit local copy pattern instead of returning &<range-variable> directly for clarity and to avoid loop-variable capture confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/access_controls_service.go` around lines 37 - 43, The code
takes the address of the for-range variable `config` when assigning to `appAcls`
(e.g., appAcls = &config) which can be confusing and error-prone due to
loop-variable semantics; fix it by creating an explicit local copy (e.g., copy
:= config) inside the loop before assigning its address to `appAcls`, and do the
same in both match branches (the domain-match branch and the app-name branch
where you call service.log.App.Debug and then set appAcls) so the pointer refers
to a stable, clearly named local variable.
internal/controller/proxy_controller_test.go (1)

367-405: ⚡ Quick win

Add at least one controller-level case for the deny ACL policy.

These tests wire the new policy engine correctly, but they still only exercise the default policy. Since the PR's main behavior is deny-by-default, please cover one unauthenticated request and one explicitly authorized request with the policy set to deny.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/proxy_controller_test.go` around lines 367 - 405, Add
controller-level test cases that exercise the deny-by-default ACL: in the tests
loop that creates the Gin router and calls controller.NewProxyController(log,
runtime, group, aclsService, authService, policyEngine), configure the policy
engine to use the deny default and add two cases—one unauthenticated request
expected to be denied (HTTP 403/401) and one request explicitly authorized via
the ACLs/policy (e.g., using aclsService or injecting an allow rule into
policyEngine) expected to succeed; ensure the new cases are added to the
existing tests slice and use the same setup (router, middlewares, recorder) so
they run with the controller instance created by NewProxyController.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/bootstrap/service_bootstrap.go`:
- Around line 51-82: The getLabelProvider currently falls back to Docker for any
unrecognized app.config.LabelProvider; change it to explicitly accept only
"none", "kubernetes", "docker", or "auto" and return an error for any other
value. Update the selection logic in getLabelProvider: compute useKubernetes as
before, compute useDocker as app.config.LabelProvider == "docker" ||
(app.config.LabelProvider == "auto" && !inCluster), and if neither matches
return fmt.Errorf("unknown label provider: %q", app.config.LabelProvider). Keep
the existing initialization calls (service.NewKubernetesService and
service.NewDockerService) and assignments to
app.services.kubernetesService/app.services.dockerService unchanged.
- Line 25: The code passes &labelProvider (a pointer-to-interface) into
NewAccessControlsService, which leads to a non-nil pointer that may hold a nil
interface and causes a panic in GetAccessControls; update the handling so
callers and service check the actual interface value: either (A) change
getLabelProvider() to return a concrete pointer type (avoid
pointer-to-interface) and propagate that through NewAccessControlsService and
the service struct, or (B) keep the current signature but change the nil checks
in GetAccessControls (and any callers) to verify both pointer and the underlying
interface—i.e., ensure you test service.labelProvider != nil &&
*service.labelProvider != nil before calling
(*service.labelProvider).GetLabels(); adjust NewAccessControlsService and the
labelProvider variable usage accordingly to eliminate the pointer-to-interface
anti-pattern.

In `@internal/service/access_controls_rules.go`:
- Around line 27-35: The Evaluate method on UserAllowedRule (and other rule
Evaluate methods) dereferences ctx and nested fields like ctx.UserContext and
ctx.IP without full nil guards, risking nil-pointer panics; add explicit nil
checks before any nested access (e.g., verify ctx != nil, then ctx.UserContext
!= nil, and ctx.ACLs != nil) in UserAllowedRule.Evaluate and the other mentioned
Evaluate implementations so you only call utils.CheckFilter or access
ctx.IP/ctx.UserContext when those pointers are non-nil; return EffectAbstain or
an appropriate effect early when a required nested field is nil to short-circuit
safely.

In `@internal/service/access_controls_service.go`:
- Around line 34-45: The current loop over service.config.Apps can pick an
app-name fallback before finding an exact domain match because map iteration is
unordered; change the logic in the function using service.config.Apps (and
assigning to appAcls) to perform two passes: first iterate all entries and check
if config.Config.Domain == domain and set appAcls (logging via
service.log.App.Debug().Str("name", app).Msg(...)) and break on exact match, and
only if no exact match found do a second pass that checks strings.SplitN(domain,
".", 2)[0] == app to set the app-name fallback; ensure appAcls is only set by
the fallback when the exact pass found nothing.

---

Outside diff comments:
In `@internal/controller/proxy_controller.go`:
- Around line 114-133: The rule evaluation calls (policyEngine.Evaluate) are
treating malformed rules as simple booleans, hiding config errors; update the
three evaluations for service.RuleIPBypassed, service.RuleAuthEnabled, and
service.RuleIPAllowed to capture and check an error return (e.g., allowed, err
:= controller.policyEngine.Evaluate(...)); if err != nil call
controller.handleError(c, err) and return; only proceed to controller.setHeaders
and c.JSON or the !allowed branch when err is nil and the boolean result is
definitive. Ensure you propagate errors from the policy engine API (or change
Evaluate to return (bool, error) if needed) and keep existing handlers
(controller.setHeaders, controller.log.App.Debug, c.JSON) unchanged when no
error occurs.

---

Duplicate comments:
In `@internal/service/access_controls_rules.go`:
- Around line 189-192: The merge of IP lists using append (creating blockedIps
and allowedIPs from ctx.ACLs.IP.Block/Allow and rule.Config.Auth.IP.Block/Allow)
can mutate the backing arrays of the shared ctx.ACLs slices; allocate a fresh
slice and copy both source slices into it instead of appending into an existing
backing array—e.g. create a new slice with capacity equal to
len(ctx.ACLs.IP.Block)+len(rule.Config.Auth.IP.Block) (or start from
append([]string{}, ctx.ACLs.IP.Block...) and then append the rule slice), and do
the same for allowedIPs; update the variables blockedIps and allowedIPs
accordingly so ctx.ACLs.IP.* is never mutated.

---

Nitpick comments:
In `@internal/controller/proxy_controller_test.go`:
- Around line 367-405: Add controller-level test cases that exercise the
deny-by-default ACL: in the tests loop that creates the Gin router and calls
controller.NewProxyController(log, runtime, group, aclsService, authService,
policyEngine), configure the policy engine to use the deny default and add two
cases—one unauthenticated request expected to be denied (HTTP 403/401) and one
request explicitly authorized via the ACLs/policy (e.g., using aclsService or
injecting an allow rule into policyEngine) expected to succeed; ensure the new
cases are added to the existing tests slice and use the same setup (router,
middlewares, recorder) so they run with the controller instance created by
NewProxyController.

In `@internal/service/access_control_rules_test.go`:
- Around line 12-181: Add regression table rows to TestUserAllowedRule (and the
other rule tests mentioned) to cover nil inputs: create two cases — one with ctx
== nil and one with ctx != nil but ctx.UserContext == nil — and assert the
rule's Evaluate method (UserAllowedRule.Evaluate) returns a deterministic Effect
(use EffectAbstain) for both; repeat the same two-row additions for each
corresponding test function covering the other rule types so Evaluate handles
nil ACLContext and nil UserContext without panicking.

In `@internal/service/access_controls_service.go`:
- Around line 37-43: The code takes the address of the for-range variable
`config` when assigning to `appAcls` (e.g., appAcls = &config) which can be
confusing and error-prone due to loop-variable semantics; fix it by creating an
explicit local copy (e.g., copy := config) inside the loop before assigning its
address to `appAcls`, and do the same in both match branches (the domain-match
branch and the app-name branch where you call service.log.App.Debug and then set
appAcls) so the pointer refers to a stable, clearly named local variable.

In `@internal/utils/security_utils.go`:
- Around line 84-93: When a filter is wrapped in slashes (the code branch that
checks strings.HasPrefix(filter, "/") && strings.HasSuffix(filter, "/")), ensure
you treat it exclusively as a regex: validate that the inner pattern is
non-empty (e.g., len(filter) > 2 and filter[1:len(filter)-1] != "") to avoid
compiling "/" or "//" into an empty regex, compile the pattern and if
compilation fails return the error, and if compilation succeeds but
re.MatchString(input) is false return (false, nil) immediately instead of
falling through to the comma-separated logic; update the existing handling
around variables filter, input and the regexp.Compile call to enforce these
checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 282799e8-3593-4bd5-9e7f-b5a47b9b4912

📥 Commits

Reviewing files that changed from the base of the PR and between f8b0188 and f9fd457.

📒 Files selected for processing (12)
  • internal/bootstrap/app_bootstrap.go
  • internal/bootstrap/router_bootstrap.go
  • internal/bootstrap/service_bootstrap.go
  • internal/controller/proxy_controller.go
  • internal/controller/proxy_controller_test.go
  • internal/service/access_control_rules_test.go
  • internal/service/access_controls_rules.go
  • internal/service/access_controls_service.go
  • internal/service/auth_service.go
  • internal/service/policy_engine.go
  • internal/utils/security_utils.go
  • internal/utils/security_utils_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/service/auth_service.go

Comment thread internal/bootstrap/service_bootstrap.go
Comment thread internal/bootstrap/service_bootstrap.go
Comment thread internal/service/access_controls_rules.go
Comment thread internal/service/access_controls_service.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
internal/service/access_controls_rules.go (1)

27-29: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add complete nil guards before all context dereferences.

Several Evaluate methods can panic on nil ctx/nested fields (e.g., ctx.ACLs and provider-specific contexts). This is still a crash-risk on auth paths and should be short-circuited consistently.

Suggested hardening pattern
 func (rule *UserAllowedRule) Evaluate(ctx *ACLContext) Effect {
-	if ctx.ACLs == nil || ctx.UserContext == nil {
+	if ctx == nil || ctx.ACLs == nil || ctx.UserContext == nil {
 		return EffectAbstain
 	}
+	if ctx.UserContext.Provider == model.ProviderOAuth && ctx.UserContext.OAuth == nil {
+		return EffectAbstain
+	}
@@
 func (rule *OAuthGroupRule) Evaluate(ctx *ACLContext) Effect {
-	if ctx.ACLs == nil || ctx.UserContext == nil {
+	if ctx == nil || ctx.ACLs == nil || ctx.UserContext == nil {
 		return EffectAbstain
 	}
@@
 func (rule *LDAPGroupRule) Evaluate(ctx *ACLContext) Effect {
-	if ctx == nil || ctx.UserContext == nil {
+	if ctx == nil || ctx.ACLs == nil || ctx.UserContext == nil {
 		return EffectAbstain
 	}
@@
 func (rule *AuthEnabledRule) Evaluate(ctx *ACLContext) Effect {
-	if ctx.ACLs == nil {
+	if ctx == nil || ctx.ACLs == nil {
 		return EffectDeny
 	}
@@
 func (rule *IPAllowedRule) Evaluate(ctx *ACLContext) Effect {
-	if ctx.ACLs == nil {
+	if ctx == nil || ctx.ACLs == nil {
 		return EffectAbstain
 	}
@@
 func (rule *IPBypassedRule) Evaluate(ctx *ACLContext) Effect {
-	if ctx.ACLs == nil {
+	if ctx == nil || ctx.ACLs == nil {
 		return EffectDeny
 	}

Also applies to: 82-84, 116-118, 145-147, 184-186, 230-232

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/access_controls_rules.go` around lines 27 - 29, Add complete
nil guards at the top of each rule's Evaluate method in access_controls_rules.go
(e.g., UserAllowedRule.Evaluate and the other Evaluate methods in this file) to
short-circuit on nil ctx or nil nested fields (ctx.ACLs, ctx.UserContext,
provider-specific subcontexts) before any dereference; return EffectAbstain on
those checks to avoid panics and keep behavior consistent across all rule
implementations.
🧹 Nitpick comments (3)
internal/service/docker_service.go (2)

89-94: ⚡ Quick win

Consider using explicit copy pattern when returning pointer to loop variable.

While returning &appLabels is safe in Go 1.22+, the explicit copy pattern improves clarity and consistency with project conventions.

📋 Proposed refactor
 		for _, appLabels := range labels.Apps {
 			if appLabels.Config.Domain == appDomain {
 				docker.log.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by domain")
-				return &appLabels, nil
+				result := appLabels
+				return &result, nil
 			}
 		}

Based on learnings: prefer the explicit local copy pattern (e.g., result := config; return &result) when returning a pointer to a loop variable for clarity and to avoid confusion about loop-variable capture semantics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/docker_service.go` around lines 89 - 94, The loop returns a
pointer to the loop variable appLabels which can be made explicit for clarity:
inside the loop over labels.Apps (the block that logs via docker.log.App.Debug()
using inspect.ID and inspect.Name), create a local copy (e.g., result :=
appLabels) and return &result instead of &appLabels so the returned pointer
clearly references a distinct local value rather than the loop variable.

97-102: ⚡ Quick win

Consider using explicit copy pattern when returning pointer to loop variable.

While returning &appLabels is safe in Go 1.22+, the explicit copy pattern improves clarity and consistency with project conventions.

📋 Proposed refactor
 		for appName, appLabels := range labels.Apps {
 			if strings.SplitN(appDomain, ".", 2)[0] == appName {
 				docker.log.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name")
-				return &appLabels, nil
+				result := appLabels
+				return &result, nil
 			}
 		}

Based on learnings: prefer the explicit local copy pattern (e.g., result := config; return &result) when returning a pointer to a loop variable for clarity and to avoid confusion about loop-variable capture semantics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/docker_service.go` around lines 97 - 102, The loop returns
the address of the loop variable appLabels (for appName, appLabels := range
labels.Apps), which can be confusing; fix by making an explicit local copy
before returning: inside the loop create a new variable (e.g., result :=
appLabels) and return &result instead of &appLabels so callers receive a stable
pointer; update the return in the matching container block where
docker.log.App.Debug() is called.
internal/service/access_controls_rules_test.go (1)

18-22: ⚡ Quick win

Add explicit ctx: nil test cases for all rule suites.

Only TestLDAPGroupRule currently asserts nil-context behavior. Adding ctx: nil cases to the other rule tables would lock in the nil-guard contract and prevent panic regressions.

Also applies to: 199-203, 447-451, 554-559, 674-678

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/access_controls_rules_test.go` around lines 18 - 22, Add an
explicit "nil context" table entry to each rule test suite (the tests :=
[]struct{...} tables in the Test... functions) similar to the existing nil-case
in TestLDAPGroupRule: add an entry with name "nil ctx", ctx: nil, and expected
set to the rule's intended Effect for a nil ACLContext (match whatever the suite
currently treats a nil/empty context as, e.g., Deny or NoEffect). Ensure you add
this entry to every rule test table in the file so each Test... function asserts
nil-context behavior and prevents panic regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/service/docker_service.go`:
- Line 88: Fix the typo in the inline comment that currently reads "fist pass -
try to find an exact match for the domain" by changing "fist" to "first" so it
reads "first pass - try to find an exact match for the domain"; locate the
comment near the domain-matching logic (the comment text itself is unique) in
docker_service.go and update it accordingly.

In `@internal/service/policy_engine_test.go`:
- Line 30: The test currently ignores errors from test.CreateTestConfigs(t)
which can hide setup failures; change the call to capture the error (e.g., cfg,
err := test.CreateTestConfigs(t)) and assert it succeeded before proceeding (use
require.NoError(t, err) or t.Fatalf on err) so failures in CreateTestConfigs are
reported and stop the test; update any imports if you choose to use
testify/require.

---

Duplicate comments:
In `@internal/service/access_controls_rules.go`:
- Around line 27-29: Add complete nil guards at the top of each rule's Evaluate
method in access_controls_rules.go (e.g., UserAllowedRule.Evaluate and the other
Evaluate methods in this file) to short-circuit on nil ctx or nil nested fields
(ctx.ACLs, ctx.UserContext, provider-specific subcontexts) before any
dereference; return EffectAbstain on those checks to avoid panics and keep
behavior consistent across all rule implementations.

---

Nitpick comments:
In `@internal/service/access_controls_rules_test.go`:
- Around line 18-22: Add an explicit "nil context" table entry to each rule test
suite (the tests := []struct{...} tables in the Test... functions) similar to
the existing nil-case in TestLDAPGroupRule: add an entry with name "nil ctx",
ctx: nil, and expected set to the rule's intended Effect for a nil ACLContext
(match whatever the suite currently treats a nil/empty context as, e.g., Deny or
NoEffect). Ensure you add this entry to every rule test table in the file so
each Test... function asserts nil-context behavior and prevents panic
regressions.

In `@internal/service/docker_service.go`:
- Around line 89-94: The loop returns a pointer to the loop variable appLabels
which can be made explicit for clarity: inside the loop over labels.Apps (the
block that logs via docker.log.App.Debug() using inspect.ID and inspect.Name),
create a local copy (e.g., result := appLabels) and return &result instead of
&appLabels so the returned pointer clearly references a distinct local value
rather than the loop variable.
- Around line 97-102: The loop returns the address of the loop variable
appLabels (for appName, appLabels := range labels.Apps), which can be confusing;
fix by making an explicit local copy before returning: inside the loop create a
new variable (e.g., result := appLabels) and return &result instead of
&appLabels so callers receive a stable pointer; update the return in the
matching container block where docker.log.App.Debug() is called.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: df6f8825-044c-4daa-a515-a2be799f98b6

📥 Commits

Reviewing files that changed from the base of the PR and between f9fd457 and f841095.

📒 Files selected for processing (8)
  • internal/bootstrap/service_bootstrap.go
  • internal/service/access_controls_rules.go
  • internal/service/access_controls_rules_test.go
  • internal/service/access_controls_service.go
  • internal/service/access_controls_service_test.go
  • internal/service/docker_service.go
  • internal/service/policy_engine.go
  • internal/service/policy_engine_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/service/access_controls_service.go
  • internal/service/policy_engine.go

Comment thread internal/service/docker_service.go Outdated
log := logger.NewLogger().WithTestConfig()
log.Init()

cfg, _ := test.CreateTestConfigs(t)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Check and assert config setup errors in the test.

cfg, _ := test.CreateTestConfigs(t) drops setup errors, which can mask root-cause failures in this suite.

Suggested fix
-	cfg, _ := test.CreateTestConfigs(t)
+	cfg, err := test.CreateTestConfigs(t)
+	assert.NoError(t, err)
+	if err != nil {
+		return
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cfg, _ := test.CreateTestConfigs(t)
cfg, err := test.CreateTestConfigs(t)
assert.NoError(t, err)
if err != nil {
return
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/policy_engine_test.go` at line 30, The test currently
ignores errors from test.CreateTestConfigs(t) which can hide setup failures;
change the call to capture the error (e.g., cfg, err :=
test.CreateTestConfigs(t)) and assert it succeeded before proceeding (use
require.NoError(t, err) or t.Fatalf on err) so failures in CreateTestConfigs are
reported and stop the test; update any imports if you choose to use
testify/require.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants